/** @module Controller */

/**
 * Base class for all controllers.
 */
export default class Controller {

	constructor( parent, object, property, className, widgetTag = 'div' ,config) {

		/**
		 * The GUI that contains this controller.
		 * @type {GUI}
		 */
		this.parent = parent;
		this.config = config ;

		/**
		 * The object this controller will modify.
		 * @type {object}
		 */
		this.object = object;

		/**
		 * The name of the property to control.
		 * @type {string}
		 */
		this.property = property;

		this.group = null ;
		this.isController = true ;

		//结束HTML
		this.$endDom = null ;

		/**
		 * Used to determine if the controller is disabled.
		 * Use `controller.disable( true|false )` to modify this value
		 * @type {boolean}
		 */
		this._disabled = false;

		/**
		 * Used to determine if the Controller is hidden.
		 * Use `controller.show()` or `controller.hide()` to change this.
		 * @type {boolean}
		 */
		this._hidden = false;

		/**
		 * The value of `object[ property ]` when the controller was created.
		 * @type {any}
		 */
		this.initialValue = this.getValue();

		//指定所属行，支持rowController或row属性
		if(config != null && config.rowController == null && config.row != null){
			config.rowController = config.row ;
		}

		/**
		 * The outermost container DOM element for this controller.
		 * @type {HTMLElement}
		 */
		this.domElement = document.createElement( 'div' );
		this.domElement.classList.add( 'controller' );
		this.domElement.classList.add( className );
		if(this.parent != null && this.parent.controllerClass != null && this.parent.controllerClass != "")
			this.domElement.classList.add(this.parent.controllerClass);
		if(this.parent != null && this.parent.controllerStyle != null && this.parent.controllerStyle != ""){
			if(config != null && config.addParentControllerStyle === false) ;
			else if(config != null && config.rowController != null) ;
			else
				this.domElement.setAttribute("style",this.parent.controllerStyle) ;
		}

		/**
		 * The DOM element that contains the controller's name.
		 * @type {HTMLElement}
		 */
		this.$name = document.createElement( 'div' );
		this.$name.classList.add( 'name' );
		if(this.parent != null && this.parent.root != null && this.parent.root.nameClass != null && this.parent.root.nameClass != "")
			this.$name.classList.add(this.parent.root.nameClass);
		if(this.parent != null && this.parent.root != null && this.parent.root.nameStyle != null && this.parent.root.nameStyle != ""){
			if(config != null && config.addParentNameStyle === false) ;
			else {
				if(this.parent.nameStyle != null && this.parent.nameStyle != "")
					this.$name.setAttribute( 'style', this.parent.nameStyle );
				else
					this.$name.setAttribute( 'style', this.parent.root.nameStyle );
			}
		}

		Controller.nextNameID = Controller.nextNameID || 0;
		this.$name.id = `lil-gui-name-${++Controller.nextNameID}`;

		/**
		 * The DOM element that contains the controller's "widget" (which differs by controller type).
		 * @type {HTMLElement}
		 */
		this.$widget = document.createElement( widgetTag );
		this.$widget.classList.add( 'widget' );

		/**
		 * The DOM element that receives the disabled attribute when using disable()
		 * @type {HTMLElement}
		 */
		this.$disable = this.$widget;


		this.$autoAddDom = true ;
		if(config != null && config.autoAddDom === false)
			this.$autoAddDom = config.autoAddDom ;
		if(this.$autoAddDom == true)
			this.domElement.appendChild( this.$name );
		this.domElement.appendChild( this.$widget );

		// Don't fire global key events while typing in a controller
		this.domElement.addEventListener( 'keydown', e => e.stopPropagation() );
		this.domElement.addEventListener( 'keyup', e => e.stopPropagation() );

		this.parent.children.push( this );
		this.parent.controllers.push( this );
		//当前归属的fieldset对象
		this.fieldset = this.parent.$fieldset ;

		if(this.$autoAddDom == true){
			if(config != null && config.rowController != null){
				this.domElement.classList.add( 'rowInlineController' );
				// config.rowController.domElement.appendChild( this.domElement );
				this.parentContriner = config.rowController.domElement ;
				this.addDomToParent(config.rowController.domElement) ;
				this.widthUnset() ;
				this.nameWidthUnset() ;
			}else {
				// this.parent.$children.appendChild( this.domElement );
				// this.addDomToParent(this.parent.$children) ;
				this.parentContriner = this.parent.getControllerContainer() ;
				this.addDomToParent(this.parentContriner) ;
			}
		}

		this._listenCallback = this._listenCallback.bind( this );

		this.name( property );

	}

	addDomToParent(parentDom){
		if(parentDom == null) return this ;
		if(parentDom.isFieldset === true || parentDom.isController === true) parentDom = parentDom.domElement ;
		if(this.config != null && this.config.insertBefore != null){
			try{
				let insertBefore = this.config.insertBefore ;
				if(insertBefore.isController === true) insertBefore = insertBefore.domElement ;
				parentDom.insertBefore( this.domElement, insertBefore ) ;
				return ;
			}catch(e){

			}
		}
		parentDom.appendChild( this.domElement );
		return this ;
	}

	/**
	 * 添加结束的HTML,部分组件支付
	 * @param html
	 * @returns {Controller}
	 * @private
	 */
	_appendEndDomHtml(html){
		if(html == null || html == "") return this ;
		if(this.$endDom == null){
			this.$endDom = document.createElement( 'span' );
			this.$endDom.classList.add("lilInlineEndSpan") ;
			this.$widget.appendChild( this.$endDom );
		}

		this.$endDom.innerHTML = html ;

		return this ;
	}

	/**
	 * Sets the name of the controller and its label in the GUI.
	 * @param {string} name
	 * @returns {this}
	 */
	name( name ) {
		if(this.$autoAddDom == false)
			return this ;
		/**
		 * The controller's name. Use `controller.name( 'Name' )` to modify this value.
		 * @type {string}
		 */
		this._name = name;
		this.$name.innerHTML = name;
		return this;
	}

	nameTip(tip){
		if(tip == null)
			tip = this.property ;
		if(this.$name == null) return this;
		this.$name.title = tip ;
		return this ;
	}

	setStyle(style){
		return this ;
	}

	setRowStyle(widgetStyle,nameStyle){
		this.setWidgetStyle(widgetStyle) ;
		this.setNameStyle(nameStyle) ;
		return this ;
	}

	setWidgetStyle(style){
		if(style == null) return this ;
		this.$widget.setAttribute("style",style) ;
		this.$widget.classList.remove("lil-gui-width-unset") ;
		return this ;
	}

	addWidgetClass(clazz){
		if(clazz == null || clazz == "") return this ;
		this.$widget.classList.add(clazz) ;
		return this ;
	}

	removeWidgetClass(clazz){
		if(clazz == null || clazz == "") return this ;
		this.$widget.classList.remove(clazz) ;
		return this ;
	}

	removeWidthUnset(){
		this.removeWidgetClass('lil-gui-width-unset') ;
		return this ;
	}

	widthUnset(){
		this.$widget.classList.add("lil-gui-width-unset") ;
		return this ;
	}

	nameWidthUnset(){
		this.$name.classList.add("lil-gui-name-width-unset") ;
		return this ;
	}

	setNameStyle(style){
		if(style == null) return this ;
		this.$name.setAttribute("style",style) ;
		return this ;
	}

	setRootStyle(style){
		if(style == null) return this ;
		this.domElement.setAttribute("style",style) ;
		return this ;
	}

	addNameClass(clazz){
		if(clazz == null || clazz == "") return this ;
		this.$name.classList.add(clazz) ;
		return this ;
	}

	removeNameClass(clazz){
		if(clazz == null || clazz == "") return this ;
		this.$name.classList.remove(clazz) ;
		return this ;
	}

	hideName(){
		this.$name.style.display = 'none';
		return this ;
	}

	showName(){
		this.$name.style.display = '';
		return this ;
	}

	addClass(clazz){
		return this ;
	}


	removeClass(clazz){
		return this ;
	}

	addRowClass(clazz){
		if(clazz == null | clazz == "") return this ;
		this.domElement.classList.add(clazz) ;
		return this ;
	}

	removeRowClass(clazz){
		if(clazz == null | clazz == "") return this ;
		this.domElement.classList.remove(clazz) ;
		return this ;
	}

	itemTooltip(data){

	}

	/**
	 * Pass a function to be called whenever the value is modified by this controller.
	 * The function receives the new value as its first parameter. The value of `this` will be the
	 * controller.
	 *
	 * For function controllers, the `onChange` callback will be fired on click, after the function
	 * executes.
	 * @param {Function} callback
	 * @returns {this}
	 * @example
	 * const controller = gui.add( object, 'property' );
	 *
	 * controller.onChange( function( v ) {
	 * 	console.log( 'The value is now ' + v );
	 * 	console.assert( this === controller );
	 * } );
	 */
	onChange( callback ) {
		/**
		 * Used to access the function bound to `onChange` events. Don't modify this value directly.
		 * Use the `controller.onChange( callback )` method instead.
		 * @type {Function}
		 */
		this._onChange = callback;
		return this;
	}

	/**
	 * Calls the onChange methods of this controller and its parent GUI.
	 * @protected
	 */
	_callOnChange() {

		this.parent._callOnChange( this );

		if ( this._onChange !== undefined ) {
			let _c = this.$eventController ;
			if(_c == null) _c = this ;
			this._onChange.call( this, this.getValue(),_c);
		}

		this._changed = true;

	}

	/**
	 * Pass a function to be called after this controller has been modified and loses focus.
	 * @param {Function} callback
	 * @returns {this}
	 * @example
	 * const controller = gui.add( object, 'property' );
	 *
	 * controller.onFinishChange( function( v ) {
	 * 	console.log( 'Changes complete: ' + v );
	 * 	console.assert( this === controller );
	 * } );
	 */
	onFinishChange( callback ) {
		/**
		 * Used to access the function bound to `onFinishChange` events. Don't modify this value
		 * directly. Use the `controller.onFinishChange( callback )` method instead.
		 * @type {Function}
		 */
		this._onFinishChange = callback;
		return this;
	}

	onFinishChangeFirst( callback ) {
		/**
		 * Used to access the function bound to `onFinishChange` events. Don't modify this value
		 * directly. Use the `controller.onFinishChange( callback )` method instead.
		 * @type {Function}
		 */
		this._onFinishChangeFirst = callback;
		return this;
	}


	/**
	 * Should be called by Controller when its widgets lose focus.
	 * @protected
	 */
	_callOnFinishChange() {

		if ( this._changed ) {

			this.parent._callOnFinishChange( this );

			if ( this._onFinishChangeFirst !== undefined ) {
				this._onFinishChangeFirst.call( this, this.getValue(), this);
			}

			if ( this._onFinishChange !== undefined ) {
				this._onFinishChange.call( this, this.getValue(), this);
			}

		}

		this._changed = false;

	}

	/**
	 * Sets the controller back to its initial value.
	 * @returns {this}
	 */
	reset() {
		this.setValue( this.initialValue );
		this._callOnFinishChange();
		return this;
	}

	addGroup(group){
		this.group = group ;
		if(group != null)
			group.addController(this) ;
		return this ;
	}

	/**
	 * Enables this controller.
	 * @param {boolean} enabled
	 * @returns {this}
	 * @example
	 * controller.enable();
	 * controller.enable( false ); // disable
	 * controller.enable( controller._disabled ); // toggle
	 */
	enable( enabled = true ) {
		return this.disable( !enabled );
	}

	/**
	 * Disables this controller.
	 * @param {boolean} disabled
	 * @returns {this}
	 * @example
	 * controller.disable();
	 * controller.disable( false ); // enable
	 * controller.disable( !controller._disabled ); // toggle
	 */
	disable( disabled = true ) {
		//先判断组是否可用
		if(this.group != null){
			//如果组是隐藏的，则该组件也不能显示
			if(this.group._disabled == true)
				disabled = true ;
		}

		if ( disabled === this._disabled ) return this;

		this._disabled = disabled;

		this.domElement.classList.toggle( 'disabled', disabled );
		this.$disable.toggleAttribute( 'disabled', disabled );

		return this;
	}

	readonly(flag = true){

		//先判断组是否可用
		if(this.group != null){
			//如果组是只读的，则该组件也只能是只读
			if(this.group._readonly == true)
				flag = true ;
		}

		this.$disable.toggleAttribute( 'readonly', flag );

		return this;
	}

	/**
	 * Shows the Controller after it's been hidden.
	 * @param {boolean} show
	 * @returns {this}
	 * @example
	 * controller.show();
	 * controller.show( false ); // hide
	 * controller.show( controller._hidden ); // toggle
	 */
	show( show = true ) {
		//先判断组是否可见
		if(this.group != null){
			//如果组是隐藏的，则该组件也不能显示
			if(this.group._hidden == true)
				show = false ;
		}
		this._hidden = !show;

		this.domElement.style.display = this._hidden ? 'none' : '';

		return this;

	}

	/**
	 * Hides the Controller.
	 * @returns {this}
	 */
	hide() {
		return this.show( false );
	}

	/**
	 * Destroys this controller and replaces it with a new option controller. Provided as a more
	 * descriptive syntax for `gui.add`, but primarily for compatibility with dat.gui.
	 *
	 * Use caution, as this method will destroy old references to this controller. It will also
	 * change controller order if called out of sequence, moving the option controller to the end of
	 * the GUI.
	 * @example
	 * // safe usage
	 *
	 * gui.add( object1, 'property' ).options( [ 'a', 'b', 'c' ] );
	 * gui.add( object2, 'property' );
	 *
	 * // danger
	 *
	 * const c = gui.add( object1, 'property' );
	 * gui.add( object2, 'property' );
	 *
	 * c.options( [ 'a', 'b', 'c' ] );
	 * // controller is now at the end of the GUI even though it was added first
	 *
	 * assert( c.parent.children.indexOf( c ) === -1 )
	 * // c references a controller that no longer exists
	 *
	 * @param {object|Array} options
	 * @returns {Controller}
	 */
	options( options ) {
		const controller = this.parent.add( this.object, this.property, options );
		controller.name( this._name );
		this.destroy();
		return controller;
	}

	/**
	 * Sets the minimum value. Only works on number controllers.
	 * @param {number} min
	 * @returns {this}
	 */
	// eslint-disable-next-line no-unused-vars
	min( min ) {
		return this;
	}

	/**
	 * Sets the maximum value. Only works on number controllers.
	 * @param {number} max
	 * @returns {this}
	 */
	// eslint-disable-next-line no-unused-vars
	max( max ) {
		return this;
	}

	/**
	 * Values set by this controller will be rounded to multiples of `step`. Only works on number
	 * controllers.
	 * @param {number} step
	 * @returns {this}
	 */
	// eslint-disable-next-line no-unused-vars
	step( step ) {
		return this;
	}

	/**
	 * Rounds the displayed value to a fixed number of decimals, without affecting the actual value
	 * like `step()`. Only works on number controllers.
	 * @example
	 * gui.add( object, 'property' ).listen().decimals( 4 );
	 * @param {number} decimals
	 * @returns {this}
	 */
	// eslint-disable-next-line no-unused-vars
	decimals( decimals ) {
		return this;
	}

	/**
	 * Calls `updateDisplay()` every animation frame. Pass `false` to stop listening.
	 * @param {boolean} listen
	 * @returns {this}
	 */
	listen( listen = true ) {

		/**
		 * Used to determine if the controller is currently listening. Don't modify this value
		 * directly. Use the `controller.listen( true|false )` method instead.
		 * @type {boolean}
		 */
		this._listening = listen;

		if ( this._listenCallbackID !== undefined ) {
			cancelAnimationFrame( this._listenCallbackID );
			this._listenCallbackID = undefined;
		}

		if ( this._listening ) {
			this._listenCallback();
		}

		return this;

	}

	_listenCallback() {

		this._listenCallbackID = requestAnimationFrame( this._listenCallback );

		// To prevent framerate loss, make sure the value has changed before updating the display.
		// Note: save() is used here instead of getValue() only because of ColorController. The !== operator
		// won't work for color objects or arrays, but ColorController.save() always returns a string.

		const curValue = this.save();

		if ( curValue !== this._listenPrevValue ) {
			this.updateDisplay();
		}

		this._listenPrevValue = curValue;

	}

	/**
	 * Returns `object[ property ]`.
	 * @returns {any}
	 */
	getValue() {
		return this.object[ this.property ];
	}

	/**
	 * Sets the value of `object[ property ]`, invokes any `onChange` handlers and updates the display.
	 * @param {any} value
	 * @returns {this}
	 */
	setValue( value ) {
		this.object[ this.property ] = value;
		this._callOnChange();
		this.updateDisplay();
		this._callOnFinishChange() ;
		return this;
	}

	/**
	 * Updates the display to keep it in sync with the current value. Useful for updating your
	 * controllers when their values have been modified outside of the GUI.
	 * @returns {this}
	 */
	updateDisplay() {
		return this;
	}

	tooltip(str) {
		if(str == null || str == "") return this ;
		this.$widget.title = str ;
		return this ;
	}

	placeholder(str) {
		return this ;
	}

	refreshControllers() {
		let controllers = this.parent.controllers ;
		for(let i=0; i<controllers.length; i++){
			let c = controllers[i] ;
			c.updateDisplay();
		}
	}

	load( value ) {
		this.setValue( value );
		this._callOnFinishChange();
		return this;
	}

	save() {
		return this.getValue();
	}

	/**
	 * Destroys this controller and removes it from the parent GUI.
	 */
	destroy() {
		try{
			this.listen( false );
			this.parent.children.splice( this.parent.children.indexOf( this ), 1 );
			this.parent.controllers.splice( this.parent.controllers.indexOf( this ), 1 );
			if(this.$eventController != null)
				this.$eventController.$span.removeChild( this.domElement );
			else if(this.parentContriner != null)
				this.parentContriner.removeChild( this.domElement );
			else
				this.parent.$children.removeChild( this.domElement );
			if(this.group != null)
				this.group.removeController(this) ;
		}catch(e){}
	}

	checkBoolean(value,defaultValue){
		if(value == null) return defaultValue ;
		if(typeof value === "boolean")
			return value ;
		if(typeof value === "string"){
			if(value == "") return defaultValue ;
			if(value == "true") return true ;
			if(value == "false") return false ;
		}
		return defaultValue ;
	}

	/**
	 * 仅颜色组件有效
	 */
	setSimple(){
		return this ;
	}

	triggerValueChange(){
		return this ;
	}
}
